// Copyright 2007 Bret Taylor
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;

namespace Notable {
  public partial class MainForm : Form {
    Preferences preferences_;

    // Load our preferences and the notes saved to disk.
    public MainForm() {
      InitializeComponent();
      ResizeColumns();

      // Localize our form strings
      this.Text = Application.ProductName;
      fileMenuItem_.Text = Messages.MainFile;
      newMenuItem_.Text = Messages.MainNewNote;
      archiveMenuItem_.Text = Messages.MainArchiveNote;
      exportMenuItem_.Text = Messages.MainExportNote;
      deleteMenuItem_.Text = Messages.MainDeleteNote;
      viewInWordMenuItem_.Text = Messages.MainViewInWord;
      exitMenuItem_.Text = Messages.MainExit;
      boldMenuItem_.Text = Messages.MainBold;
      italicMenuItem_.Text = Messages.MainItalic;
      underlineMenuItem_.Text = Messages.MainUnderline;
      strikethroughMenuItem_.Text = Messages.MainStrikethrough;
      fixedWidthMenuItem_.Text = Messages.MainFixedWidth;
      helpMenuItem_.Text = Messages.MainHelp;
      aboutMenuItem_.Text = String.Format(Messages.MainAbout, Application.ProductName);
      reportBugMenuItem_.Text = Messages.MainReportBug;
      homePageMenuItem_.Text = String.Format(Messages.MainHomepage, Application.ProductName);
      listView_.Columns[0].Text = Messages.MainNoteName;
      listView_.Columns[1].Text = Messages.MainDate;
      archiveContextMenuItem_.Text = Messages.MainArchiveNote;
      exportContextMenuItem_.Text = Messages.MainExportNote;
      deleteContextMenuItem_.Text = Messages.MainDeleteNote;
      viewInWordContextMenuItem_.Text = Messages.MainViewInWord;

      // Restore the application in its last position based on the saved
      // preferences file
      preferences_ = LoadPreferences();
      bool firstTime = preferences_ == null;
      if (firstTime) {
        preferences_ = new Preferences();
        this.StartPosition = FormStartPosition.WindowsDefaultLocation;
      } else {
        this.StartPosition = FormStartPosition.Manual;
        this.Location = new Point(preferences_.Left, preferences_.Top);
      }
      this.Size = new Size(preferences_.Width, preferences_.Height);
      topPanel_.Height = preferences_.ListViewHeight;

      // Load all of the notes from disk, or create a new note if there are no
      // saved notes
      List<Note> notes = LoadNotes();
      if (notes.Count > 0) {
        foreach (Note note in notes) {
          LoadNote(note);
        }
      } else if (!firstTime || !LoadSpecialNote(Messages.IntroductionNoteFileName)) {
        NewNote();
      }

      // Show the upgrade note if this is the first time the user has run the
      // application since they upgraded
      if (preferences_.Version != Application.ProductVersion) {
        preferences_.Version = Application.ProductVersion;
        LoadSpecialNote(Messages.UpgradeNoteFileName);
        preferences_.Save();
      }

      // Start the auto-save timer
      timer_.Start();
    }

    /// <summary>
    ///  Displays the given error message to the end user.
    /// </summary>
    private void ShowError(string message) {
      MessageBox.Show(this, message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
    }

    /// <summary>
    /// Resize the list view columns so that they expand to the width of the
    /// window.
    /// </summary>
    private void ResizeColumns() {
      WinUser.SCROLLINFO scrollInfo = new WinUser.SCROLLINFO();
      scrollInfo.fMask = WinUser.SIF_ALL;
      bool scrollBars = false;
      if (WinUser.GetScrollInfo(listView_.Handle, WinUser.SB_VERT, ref scrollInfo)) {
        scrollBars = scrollInfo.nPage <= scrollInfo.nMax;
      }
      int scrollWidth = scrollBars ? SystemInformation.VerticalScrollBarWidth : 0;
      titleColumnHeader_.Width = this.Width - scrollWidth - dateColumnHeader_.Width - 12;
    }

    /// <summary>
    /// Loads the saved preferences file, or returns null if the preferences
    /// file does not exist.
    /// </summary>
    private Preferences LoadPreferences() {
      try {
        return Preferences.Load();
      } catch (Exception) {
        return null;
      }
    }

    /// <summary>
    /// Move the current line to the right by 25 twips, or add a bullet if the
    /// current line doesn't have one
    /// </summary>
    private void IndentCurrentLine() {
      if (!textBox_.SelectionBullet) {
        textBox_.SelectionBullet = true;
      } else {
        textBox_.SelectionIndent += 25;
      }
      textBox_.BulletIndent = 15;
      textBox_.SelectionHangingIndent = 15;
    }

    /// <summary>
    /// Reduces the indentation of the current line, or removes the indent
    /// completely. If the current line is already completely unindented, we
    /// return false.
    /// </summary>
    private bool UnIndentCurrentLine() {
      if (textBox_.SelectionBullet) {
        if (textBox_.SelectionIndent > 0) {
          textBox_.SelectionIndent -= 25;
        } else {
          textBox_.SelectionIndent = 0;
          textBox_.SelectionBullet = false;
        }
        return true;
      }
      return false;
    }

    /// <summary>
    /// Returns true if the user is currently typing the first line (title) of
    /// the current note.
    /// </summary>
    private bool TypingFirstLine() {
      return textBox_.Text.Substring(0, textBox_.SelectionStart).IndexOf('\n') < 0;
    }

    /// <summary>
    /// Update the title of the note in the list based on the first line of
    /// the note body.
    /// </summary>
    private void UpdateTitle() {
      ListViewItem item = SelectedItem;
      if (item == null || listView_.Items.Count == 0) {
        return;
      }
      string text = textBox_.Text;
      int endLine = text.IndexOf('\n');
      string title = text;
      if (endLine >= 0) {
        title = text.Substring(0, text.IndexOf('\n'));
      }
      if (item.Text != title) {
        item.Text = title;
      }
    }

    /// <summary>
    /// Initialize the formatting and content of the text box for a new note.
    /// </summary>
    private void InitializeTextBox() {
      // Clear the text box
      textBox_.Text = "";

      // The title should be bold
      textBox_.SelectionFont = new Font("Arial", 10, FontStyle.Bold);

      // Put 2 points spacing between each line to make the note more readable
      WinUser.PARAFORMAT2 fmt = new WinUser.PARAFORMAT2();
      fmt.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(fmt);
      fmt.dwMask = WinUser.PFM_SPACEAFTER;
      fmt.dySpaceAfter = 40;
      WinUser.SendMessage(textBox_.Handle, WinUser.EM_SETPARAFORMAT, 1, ref fmt);
    }

    /// <summary>
    /// Loads all the notes from disk, returning an array of note structs.
    /// </summary>
    private List<Note> LoadNotes() {
      List<Note> notes = new List<Note>();
      try {
        foreach (string path in Directory.GetFiles(Settings.SettingsDirectory(), "*" + Note.PathSuffix)) {
          try {
            notes.Add(Note.Load(path));
          } catch (Exception e) {
            ShowError(String.Format(Messages.ErrorNoteLoad, e.Message));
          }
        }
      } catch (Exception e) {
        ShowError(String.Format(Messages.ErrorSettingsDirectory, e.Message));
      }
      notes.Sort();
      return notes;
    }

    /// <summary>
    /// Creates new note line in the list and clears the text box for composition
    /// of the note.
    /// </summary>
    private void NewNote() {
      if (listView_.SelectedItems.Count > 0) {
        listView_.SelectedItems[0].Selected = false;
      }
      InitializeTextBox();
      Note note = new Note(Guid.NewGuid(), DateTime.Now, textBox_.Rtf);
      LoadNote(note);
    }

    /// <summary>
    /// Loads the introductory note we install with the executable at
    /// installation.
    /// </summary>
    private bool LoadSpecialNote(string fileName) {
      try {
        string path = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), fileName);
        Debug.WriteLine("Loading special note: " + path);
        Note note = Note.Load(path);
        note.Guid = Guid.NewGuid().ToString();
        note.Timestamp = DateTime.Now;
        note.SetDirty(true);
        LoadNote(note);
        return true;
      } catch (Exception e) {
        Debug.WriteLine(e.Message);
        return false;
      }
    }

    /// <summary>
    /// Loads the ListViewItem for the given note and selects it, which triggers
    /// the loading of the text box.
    /// </summary>
    private void LoadNote(Note note) {
      string[] subItems = new string[2];
      subItems[0] = "";
      subItems[1] = note.Timestamp.ToString("ddd, MMM d");
      ListViewItem item = new ListViewItem(subItems);
      listView_.Items.Add(item);
      item.Tag = note;
      item.Selected = true;
      ResizeColumns();
    }

    /// <summary>
    /// Iterates over all of the notes, saving those that have the dirty bit
    /// set.
    /// </summary>
    private void SaveNotes() {
      foreach (ListViewItem item in listView_.Items) {
        try {
          Note note = (Note) item.Tag;
          if (note.IsDirty()) {
            note.Save();
            note.SetDirty(false);
          }
        } catch (Exception e) {
          ShowError(e.Message);
        }
      }
    }

    /// <summary>
    /// Saves our global settings to the application data directory.
    /// </summary>
    private void AutoSavePreferences() {
      if (preferences_.Width != this.Width || preferences_.Height != this.Height ||
          preferences_.Top != this.Top || preferences_.Left != this.Left ||
          preferences_.ListViewHeight != topPanel_.Height) {
        preferences_.Width = this.Width;
        preferences_.Height = this.Height;
        preferences_.Left = this.Left;
        preferences_.Top = this.Top;
        preferences_.ListViewHeight = topPanel_.Height;
        try {
          preferences_.Save();
        } catch (Exception e) {
          ShowError(e.Message);
        }
      }
    }

    /// <summary>
    /// Exports and then deletes the note. If the user cancels or the export,
    /// or if the export fails, we do not delete the note.
    /// </summary>
    private void ArchiveNote() {
      if (ExportNote()) {
        DeleteNote(false);
      }
    }

    /// <summary>
    /// Deletes the given note from the list with a prompt.
    /// </summary>
    private void DeleteNote() {
      DeleteNote(true);
    }

    /// <summary>
    /// Deletes the given note from the list, and deletes the file as well.
    /// If confirm is true, we confirm the deletion with the user first.
    /// </summary>
    private void DeleteNote(bool confirm) {
      // If requested, confirm from the user first
      if (confirm && MessageBox.Show(this, Messages.ConfirmNoteDelete, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) {
        return;
      }

      ListViewItem item = SelectedItem;
      Note note = SelectedNote;
      int newIndex = item.Index;
      listView_.Items.Remove(item);
      note.Delete();

      // Select the note next to the deleted one
      if (listView_.Items.Count > 0) {
        if (newIndex < listView_.Items.Count) {
          listView_.Items[newIndex].Selected = true;
        } else {
          listView_.Items[listView_.Items.Count - 1].Selected = true;
        }
      }

      ResizeColumns();
    }

    /// <summary>
    /// Shows the user a file dialog so they can save the note to a standalone
    /// file. Returns true if the operation was not cancelled and completed
    /// successfully.
    /// </summary>
    private bool ExportNote() {
      // Set the default file name and strip illegal characters
      ListViewItem item = SelectedItem;
      Note note = SelectedNote;
      string fileName = item.SubItems[0].Text + " - " + String.Format("{0:yyyy-MM-dd}", note.Timestamp) + ".rtf";
      foreach (char illegal in Path.GetInvalidFileNameChars()) {
        fileName = fileName.Replace(illegal, ' ');
      }
      saveFileDialog_.FileName = fileName;
      if (saveFileDialog_.ShowDialog(this) == DialogResult.OK) {
        return SaveNoteTo(saveFileDialog_.FileName);
      }
      return false;
    }

    /// <summary>
    /// Saves a copy of the file to a temp directory and opens it in Word.
    /// </summary>
    private void OpenInWord() {
      string path = Path.GetTempPath() + Guid.NewGuid().ToString() + ".rtf";
      Debug.WriteLine("Temporary path: " + path);
      if (SaveNoteTo(path)) {
        Process.Start(path);
      }
    }

    /// <summary>
    /// Saves the current RTF to the given path, returning true if it is
    /// successful.
    /// </summary>
    private bool SaveNoteTo(string path) {
      try {
        using (StreamWriter writer = new StreamWriter(path)) {
          if (Path.GetExtension(path) == ".rtf") {
            writer.Write(textBox_.Rtf);
          } else {
            writer.Write(textBox_.Text);
          }
          return true;
        }
      } catch (Exception e) {
        ShowError(e.Message);
      }
      return false;
    }

    /// <summary>
    /// Checks or unchecks the bold/italic/underline menus.
    /// </summary>
    private void UpdateMenus() {
      Font font = this.SelectionFont;
      boldMenuItem_.Checked = font.Bold;
      italicMenuItem_.Checked = font.Italic;
      underlineMenuItem_.Checked = font.Underline;
      strikethroughMenuItem_.Checked = font.Strikeout;
      fixedWidthMenuItem_.Checked = font.FontFamily.Name == "Courier New";
    }

    /// <summary>
    /// If the version on the server is different than our version, show the
    /// update message.
    /// </summary>
    /// <param name="versionInfo"></param>
    private void OnUpdateCheck(VersionInfo versionInfo) {
      if (versionInfo.CurrentVersion != Application.ProductVersion) {
        if (MessageBox.Show(versionInfo.UpdateMessage, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) {
          Process.Start(versionInfo.UpdateUrl);
        }
      }
    }

    /// <summary>
    /// Returns the URL we should use to check for more recent versions of
    /// Notable.
    /// </summary>
    private String GetVersionCheckUrl() {
      return String.Format(Messages.UpdateCheckUrl, Application.ProductVersion);
    }

    /// <summary>
    /// Filter out Tab, Backspace, and Return keystrokes since they perform
    /// special operations on the note.
    /// </summary>
    protected override bool ProcessCmdKey(ref Message msg, Keys keyData) {
      if (keyData == Keys.Tab) {
        IndentCurrentLine();
        return true;
      }
      if (textBox_.Focused) {
        if (keyData == Keys.Back && textBox_.Focused) {
          // Only un-indent if we are at the beginning of a line
          if (textBox_.SelectionStart == 0 || textBox_.Text[textBox_.SelectionStart - 1] == '\n') {
            if (UnIndentCurrentLine()) {
              return true;
            }
          }
        } else if (keyData == Keys.Return && TypingFirstLine()) {
          // If the user just typed the title, turn off Bold for subsequent lines
          textBox_.SelectionFont = new Font(this.SelectionFont.FontFamily, this.SelectionFont.Size, FontStyle.Regular);
        }
      }
      return base.ProcessCmdKey(ref msg, keyData);
    }

    /// <summary>
    /// The currently selected ListViewItem, or null if none is selected.
    /// </summary>
    private ListViewItem SelectedItem {
      get {
        if (listView_.Items.Count == 0 || listView_.SelectedIndices.Count == 0) {
          return null;
        } else {
          return listView_.Items[listView_.SelectedIndices[0]];
        }
      }
    }

    /// <summary>
    /// The font of the selected text in the rich text box.
    /// </summary>
    private Font SelectionFont {
      get {
        if (textBox_.SelectionFont != null) {
          return textBox_.SelectionFont;
        } else {
          return textBox_.Font;
        }
      }
    }

    /// <summary>
    /// Returns the currently selected Note, or null if none is selected.
    /// </summary>
    private Note SelectedNote {
      get {
        ListViewItem item = SelectedItem;
        if (item != null) {
          return (Note) item.Tag;
        } else {
          return null;
        }
      }
    }

    /// <summary>
    /// When a new note is selected, load it into the rich text box.
    /// </summary>
    private void listView__SelectedIndexChanged(object sender, EventArgs e) {
      Note selectedNote = SelectedNote;
      if (selectedNote != null) {
        Debug.WriteLine("Editing note " + selectedNote.Guid.ToString());
        bool dirty = selectedNote.IsDirty();
        textBox_.Rtf = selectedNote.Rtf;
        selectedNote.SetDirty(dirty);
      } else {
        InitializeTextBox();
      }

      bool enabled = selectedNote != null;
      textBox_.Enabled = enabled;
      archiveMenuItem_.Enabled = enabled;
      exportMenuItem_.Enabled = enabled;
      deleteMenuItem_.Enabled = enabled;
      viewInWordMenuItem_.Enabled = enabled;
      if (noteContextMenu_.Visible) {
        noteContextMenu_.Close();
      }
      if (enabled) textBox_.Focus();
    }

    /// <summary>
    /// Auto-size the ListView columns.
    /// </summary>
    private void MainForm_Resize(object sender, EventArgs e) {
      ResizeColumns();
    }

    private void homePageMenuItem__Click(object sender, EventArgs e) {
      try {
        Process.Start(Messages.HomePageUrl);
      } catch (Exception exception) {
        ShowError(exception.Message);
      }
    }

    /// <summary>
    /// Update the title and dirty bit of the current note.
    /// </summary>
    private void textBox__TextChanged(object sender, EventArgs e) {
      Note selectedNote = SelectedNote;
      if (selectedNote != null) {
        selectedNote.Rtf = textBox_.Rtf;
        selectedNote.SetDirty(true);
      }
      UpdateTitle();
    }

    /// <summary>
    ///  Save the notes and preferences periodically.
    /// </summary>
    private void timer__Tick(object sender, EventArgs e) {
      SaveNotes();
      AutoSavePreferences();
    }

    /// <summary>
    /// Save the notes and preferences always before we exit.
    /// </summary>
    private void MainForm_FormClosing(object sender, FormClosingEventArgs e) {
      SaveNotes();
      AutoSavePreferences();
    }

    private void newMenuItem__Click(object sender, EventArgs e) {
      NewNote();
    }

    private void archiveMenuItem__Click(object sender, EventArgs e) {
      ArchiveNote();
    }

    private void exportMenuItem__Click(object sender, EventArgs e) {
      ExportNote();
    }

    private void deleteMenuItem__Click(object sender, EventArgs e) {
      DeleteNote();
    }

    private void viewInWordMenuItem__Click(object sender, EventArgs e) {
      OpenInWord();
    }

    private void exitMenuItem__Click(object sender, EventArgs e) {
      this.Close();
    }

    private void aboutMenuItem__Click(object sender, EventArgs e) {
      AboutDialog dialog = new AboutDialog();
      dialog.ShowDialog(this);
    }

    private void boldMenuItem__Click(object sender, EventArgs e) {
      textBox_.SelectionFont = new Font(this.SelectionFont, this.SelectionFont.Style ^ FontStyle.Bold);
      UpdateMenus();
    }

    private void italicMenuItem__Click(object sender, EventArgs e) {
      textBox_.SelectionFont = new Font(this.SelectionFont, this.SelectionFont.Style ^ FontStyle.Italic);
      UpdateMenus();
    }

    private void underlineMenuItem__Click(object sender, EventArgs e) {
      textBox_.SelectionFont = new Font(this.SelectionFont, this.SelectionFont.Style ^ FontStyle.Underline);
      UpdateMenus();
    }

    private void strikethroughMenuItem__Click(object sender, EventArgs e) {
      Color color = this.SelectionFont.Strikeout ? Color.Black : Color.DarkGray;
      textBox_.SelectionFont = new Font(this.SelectionFont, this.SelectionFont.Style ^ FontStyle.Strikeout);
      textBox_.SelectionColor = color;
      UpdateMenus();
    }

    private void fixedWidthMenuItem__Click(object sender, EventArgs e) {
      if (this.SelectionFont.FontFamily.Name == "Courier New") {
        textBox_.SelectionFont = new Font("Arial", 10, this.SelectionFont.Style);
      } else {
        textBox_.SelectionFont = new Font("Courier New", 10, this.SelectionFont.Style);
      }
      UpdateMenus();
    }

    private void textBox__SelectionChanged(object sender, EventArgs e) {
      UpdateMenus();
    }

    private void MainForm_Load(object sender, EventArgs e) {
      // Check for new versions of Notable
      UpdateChecker checker = new UpdateChecker(GetVersionCheckUrl(), new UpdateChecker.Callback(OnUpdateCheck));
      checker.Run();
    }

    private void textBox__LinkClicked(object sender, LinkClickedEventArgs e) {
      try {
        Process.Start(e.LinkText);
      } catch (Exception exception) {
        ShowError(exception.Message);
      }
    }

    private void topPanel__SizeChanged(object sender, EventArgs e) {
      ResizeColumns();
    }

    private void archiveContextMenuItem_Click(object sender, EventArgs e) {
      ArchiveNote();
    }

    private void exportContextMenuItem_Click(object sender, EventArgs e) {
      ExportNote();
    }

    private void deleteContextMenuItem_Click(object sender, EventArgs e) {
      DeleteNote();
    }

    private void viewInWordContextMenuItem_Click(object sender, EventArgs e) {
      OpenInWord();
    }

    private void listView__MouseUp(object sender, MouseEventArgs e) {
      if (e.Button == MouseButtons.Right) {
        if (SelectedItem != null) {
          noteContextMenu_.Show(listView_, e.Location);
        }
      }
    }

    private void reportBugMenuItem__Click(object sender, EventArgs e) {
      try {
        Process.Start(Messages.ReportBugUrl);
      } catch (Exception exception) {
        ShowError(exception.Message);
      }
    }
  }
}
